Skip to content
Redirected from Dev ApiKey
created by Aha00aAha00a at 2026-06-24
last modified by Aha00aAha00a at 2026-06-25
revision: 6

Dev Api

외부 자동화 도구가 AhaWiki 페이지를 직접 수정할 수 있도록 사용자 개인 API Key 인증을 추가했다.

세션 쿠키와 reCAPTCHA 없이 Authorization: Bearer <key> 헤더로 페이지를 읽고 저장할 수 있는 API를 제공한다.

페이지 히스토리에는 편집자가 기존 사용자로 기록되며, API Key를 통한 편집은 Page.viaApi = TRUE로 표시한다.

1. 설계 결정

  • API Key는 기존 사용자 계정에 직접 연결한다. 자동화 전용 계정을 별도로 만들지 않는다.
  • API Key로 저장한 revision은 Page.viaApi = TRUE로 기록한다.
  • API Key 원문은 DB에 저장하지 않고 SHA-256 hash만 저장한다. 원문 key는 생성 응답에서 한 번만 보여준다.
  • AhaWiki API는 Bearer 인증을 사용하므로 CSRF token과 reCAPTCHA를 요구하지 않는다.
  • AhaWiki API의 읽기/쓰기 권한은 기존 WikiPermission 규칙과 key 소유자 사용자의 권한을 그대로 따른다.
  • API Key 관리는 사용자가 Account Settings에서 직접 한다. Admin은 전체 key 조회와 강제 폐기만 할 수 있다.

2. Data Model

2.1. UserApiKey

UserApiKey 테이블은 사용자별 API Key 메타데이터와 hash를 저장한다.

  • seq BIGINT AUTO_INCREMENT PRIMARY KEY
  • user INT NOT NULLUser.seq FK
  • keyHash VARCHAR(64) NOT NULL UNIQUE — SHA-256 hex
  • keyPrefix VARCHAR(32) NOT NULL — 목록에서 key를 식별하기 위한 prefix
  • label VARCHAR(255) NOT NULL
  • dateInserted DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  • dateLastUsed DATETIME NULL
  • dateRevoked DATETIME NULL
  • INDEX (user, dateRevoked) — 사용자별 key 목록 조회

keyHash UNIQUE가 인증 조회 인덱스 역할을 하므로 별도 INDEX (keyHash)는 만들지 않는다.

2.2. Page.viaApi

Page 테이블에 viaApi BOOLEAN NOT NULL DEFAULT FALSE 컬럼을 추가했다.

  • 일반 웹 편집은 기본값 FALSE다.
  • API Key 저장만 TRUE로 기록한다.
  • Page, PageWithoutContent, row parser, history 조회, insert SQL에 모두 viaApi를 포함한다.
  • PageLogic.insertviaApi: Boolean = false 기본값을 받고, API Key 저장 시 true를 전달한다.

3. 인증과 권한

SessionLogic.getApiKeyUser(request)Authorization: Bearer <key> 헤더를 읽고 raw key를 SHA-256으로 hash한 뒤 UserApiKey에서 활성 key를 조회한다.

인증 성공 시:

  • dateLastUsed를 갱신한다.
  • key 소유자의 User.SessionUser를 만든다.
  • primary email을 함께 담아 기존 email 기반 permission과 호환한다.

AhaWiki API에서는 RequestWrapper.forUser(user)로 인증 사용자를 ContextWikiPageWikiPermission에 전달한다.

이 처리가 없으면 인증은 성공해도 권한 계산이 익명 사용자 기준으로 동작할 수 있다.

4. API와 UI

4.1. AhaWiki API

외부 사용 설명서는 Api에 둔다.

  • GET /api/v1/page/*nameEncoded
  • POST /api/v1/page/*nameEncoded
  • GET /api/v1/pages
  • POST /api/v1/pages/metadata
  • GET /api/v1/changes
  • POST /api/v1/rename
  • DELETE /api/v1/page/*nameEncoded

저장 API는 JSON body의 revision, text, comment, minorEdit를 사용한다.

revision이 최신과 다르면 409 Conflict를 반환한다.

현재 API 저장은 PageLogic.insert(..., viaApi = true), cache invalidate, page calculation enqueue까지만 수행한다.

웹 편집의 websocket broadcast와 Telegram 알림은 보내지 않는다.

Telegram 알림은 minorEditviaApi 저장을 제외한다.

4.2. AhaWikiDoc Sync 지원

AhaWikiDoc sync를 위해 AhaWiki API를 보강했다.

  • 페이지 목록/메타 API는 name, revision, dateTime, isMinorEdit, viaApi, contentHash를 반환한다.
  • Batch metadata API는 여러 page name의 revision, dateTime, hash를 한 번에 조회한다.
  • 최근 변경 API는 API Key 인증으로 page prefix, timestamp, page별 revision 기준 필터를 지원한다.
  • afterRevision은 페이지별 revision이므로 name으로 단일 페이지를 지정한 경우에만 허용한다.
  • 이름변경 API는 revision 확인 후 기존 페이지를 rename하고, 기존 이름에는 viaApi = true redirect page를 만든다.
  • 삭제 API는 revision과 confirm: true를 요구하며, 웹 삭제와 같은 정책으로 첨부파일도 삭제 처리한다.
  • sync state는 서버에 저장하지 않고 local manifest에 lastSyncedAt, page별 revision, dateTime, contentHash를 기록하는 방식을 권장한다.
  • 문서 rename sync는 delete+create가 아니라 POST /api/v1/rename으로 기존 page history를 보존한다.

4.3. Account Settings

Account Settings에 API Key 관리 섹션을 추가했다.

  • 내 API Key 목록
  • label 기반 key 생성
  • 생성 직후 plain text key 1회 표시와 복사 버튼
  • key 폐기 버튼

목록 조회에서는 plain text key를 반환하지 않고 keyPrefix만 보여준다.

세션 기반 내부 API:

  • GET /api/account/ApiKeys
  • POST /api/account/ApiKeys
  • DELETE /api/account/ApiKeys/:seq

이 API는 로그인 세션과 CSRF token이 필요하다.

POST 응답에만 plain text key를 포함하고, 이후 조회에서는 keyPrefix만 반환한다.

4.4. Admin UI

Admin SPA에 /Admin/ApiKeys 화면을 추가했다.

  • 전체 API Key 목록
  • user nickname, label, key prefix, 생성일, 마지막 사용일, 폐기 상태
  • Admin 강제 폐기

세션 기반 내부 Admin API:

  • GET /api/Admin/ApiKeys
  • DELETE /api/Admin/ApiKeys/:seq

Admin 권한과 CSRF token이 필요하다.

5. 변경 이력 표시

viaApi는 사용자가 자동화 편집을 구분할 수 있도록 여러 화면과 API에 노출한다.

  • Api.change 응답에 viaApi 포함
  • Api.adminRecentChanges 응답에 viaApi 포함
  • Wiki history 화면에 minor edit, Via API 이모지 컬럼과 Show ViaApi 필터 추가
  • RecentChanges 매크로에 Include via API edits 토글과 minor edit, Via API 이모지 컬럼 추가
  • /api/changeincludeViaApi 파라미터 추가
  • Admin Recent Changes 페이지에 viaApi 포함/제외 토글과 minor edit, Via API 이모지 컬럼 추가
  • Admin Dashboard의 Recent Changes 요약 표에 minor edit, Via API 이모지 컬럼 추가

6. Security

  • API Key는 SecureRandom으로 32 bytes를 생성하고 Base64 URL-safe 문자열로 표시한다. 형식은 ahawiki_<token>이다.
  • DB에는 raw key를 저장하지 않는다.
  • 고엔트로피 API Key이므로 SHA-256 hash로 비교한다. bcrypt/Argon2 같은 slow hash는 요청마다 불필요한 지연을 만든다.
  • AhaWiki API는 /api/v1/ 경로에서 CSRF filter를 우회한다.
  • 브라우저 세션 기반 Account/Admin API는 기존 CSRF 정책을 유지한다.
  • 별도 API Key 단위 rate limit은 1차 구현에 포함하지 않았다. 현재 전역 IP rate limit은 AhaWiki API에도 적용된다.

7. Tests

  • ApiV1Spec
    • API Key hash 저장
    • 유효 key 인증 성공
    • 폐기 key와 존재하지 않는 key 인증 실패
    • 읽기/쓰기 권한 403
    • API 읽기
    • API 저장과 viaApi = TRUE
    • revision 충돌 409 Conflict
    • 페이지 목록/메타 조회
    • 최근 변경의 since, includeMinorEdit, includeViaApi, invalid since 검증
    • afterRevision을 단일 페이지에만 허용하는 정책 검증
    • API 이름변경과 redirect 생성
    • API 삭제와 첨부파일 삭제 표시
    • Account API 생성/목록/폐기
  • ApiV1FilterSpec
    • 실제 Filters 체인에서 /api/v1/ POST가 CSRF token 없이 Bearer 인증만으로 저장되는지 검증
  • UnitTestSuiteSpec
    • 테스트용 수동 Page schema에 viaApi 컬럼 추가

8. 검증

관련 테스트:

  • sbt.bat "testOnly com.aha00a.controllers.ApiV1Spec"
  • sbt.bat "testOnly com.aha00a.controllers.ApiV1FilterSpec"

마지막 확인 시 ApiV1Spec 18개 테스트가 통과했다.

9. See Also

9.2. Similar Pages

Similar pages by cosine similarity. Words after page name are term frequency.

  • Same Wiki
    • 82.52% Api api(86:76), key(35:22), page(17:29), via(25:20), wiki(16:12), v1(10:15), revision(7:18), minor(8:16), aha(12:11), edit(6:16)
    • 72.94% ToDo ApiKey api(86:29), key(35:9), via(25:2), page(17:8), wiki(16:2), revision(7:7), aha(12:1), account(9:4), api는(11:1), minor(8:3)
  • Sister Wikis
    • 36.76% Aha00a:RecentChanges api(86:2), via(25:2), page(17:3), wiki(16:1), user(12:1), minor(8:2), date(8:1), revision(7:1), changes(5:3), edit(6:1)
    • 35.18% Aha00a:PlantUML api(86:30), include(4:18), aha(12:1), sync(3:3), request(2:3), content(3:1), insert(2:1), pages(2:1), primary(2:1), invalidate(1:1)
    • 34.09% WhoHow:FrontPage api(86:2), via(25:2), page(17:2), wiki(16:1), user(12:1), minor(8:2), date(8:1), revision(7:1), changes(5:3), recent(4:4)
    • 33.24% AhariseWiki:FrontPage api(86:2), via(25:2), page(17:2), wiki(16:2), user(12:1), minor(8:2), date(8:1), changes(5:3), recent(4:4), revision(7:1)
    • 30.23% Millpoo:FrontPage api(86:2), via(25:2), page(17:2), wiki(16:3), aha(12:2), user(12:1), minor(8:2), account(9:1), date(8:1), recent(4:5)

9.3. Adjacent Pages

Control
≤ 32
all
1.0x
1.0x
80
-120
ON
Metrics
Nodes(visible/total)0/0
Links(visible/total)0/0
Avg degree0.00
Depth coverage0
Queue(fetch/graph)0 / 0
Zoom(scale)1.00x
Ctrl/⌘ + Scroll: Zoom
Root 1-hop 2-hop+